﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reflection;

namespace Tools
{
	public class UndoRedo
	{
		public UndoRedo()
		{
			LogActive = true;
		}
		
		/// <summary>Define that changes will remembered</summary>
		public bool LogActive { get; set; }
		
		/// <summary>Clear whole history</summary>
		public void Clear()
		{
			redoStack.Clear();
			undoStack.Clear();
		}
		
		/// <summary>Storable element. For internal use only</summary>
		private struct Memento
		{
			/// <summary>Property to remember</summary>
			public PropertyInfo Property { get; set; }
			
			/// <summary>Property value</summary>
			public object State { get; set; }
			
			/// <summary>Object reference</summary>
			public object Target { get; set; }
		}
		
		/// <summary>For internal use only</summary>
		private Memento SwitchMemento(Memento memento)
		{
			object target = memento.Target;
			PropertyInfo pr = memento.Property;
			object st = pr.GetValue(target, null);
			
			bool ur = LogActive;
			LogActive = false;
			
			pr.SetValue(target, memento.State, null);
			
			memento.State = st;
			LogActive = ur;
			
			return memento;
		}
		
		/// <summary>False if undo stack is empty</summary>
		public bool CanUndo { get { return undoStack.Count != 0; } }
		
		/// <summary>Undo operation</summary>
		public void Undo() { Undo(0); }
		
		/// <summary>Set of undo operations</summary>
		/// <param name="index">number of operations to call</param>
		private void Undo(int index)
		{
			if (!CanUndo) return;
			for (int i = 0; i <= index; i++)
				redoStack.Push(SwitchMemento((Memento)undoStack.Pop()));
		}
		
		/// <summary>False if redo stack is empty</summary>
		public bool CanRedo { get { return redoStack.Count != 0; } }

		/// <summary>Redo operation</summary>
		public void Redo() { Redo(0); }
		
		/// <summary>Set of redo operations</summary>
		/// <param name="index">number of operations to call</param>
		private void Redo(int index)
		{
			if (!CanRedo) return;
			for (int i = 0; i <= index; i++)
				undoStack.Push(SwitchMemento((Memento)redoStack.Pop()));
		}

		/// <summary>Use this method to subscribe UndoRedo system to your collection.</summary>
		/// <param name="collection">Any changes the properties of the elements of the collection can be undone</param>
		public void Subscribe<T>(ObservableCollection<T> collection)
			where T : INotifyPropertyChanging
		{
			foreach (INotifyPropertyChanging element in collection) {
				element.PropertyChanging += Item_PropertyChanging;
			}
			collection.CollectionChanged += (sender, args) => {
				if (args.Action == NotifyCollectionChangedAction.Add) {
					foreach (INotifyPropertyChanging element in args.NewItems.Cast<INotifyPropertyChanging>()) {
						element.PropertyChanging += Item_PropertyChanging;
					}
				}
				if (args.Action == NotifyCollectionChangedAction.Remove) {
					foreach (INotifyPropertyChanging element in args.OldItems.Cast<INotifyPropertyChanging>()) {
						element.PropertyChanging -= Item_PropertyChanging;
					}
				}
			};
		}

		private void Item_PropertyChanging(object sender, PropertyChangingEventArgs args)
		{
			if (!LogActive) return;
			
			if (redoStack.Count != 0) redoStack.Clear();
			
			object target = sender;
			string propertyName = args.PropertyName;

			PropertyInfo property = target.GetType().GetProperty(propertyName);
			undoStack.Push(new Memento() {
			               	Target = target,
			               	Property = property,
			               	State = property.GetValue(target, null) });
		}
		
		/// <summary>Undo stack</summary>
		private Stack<Memento> undoStack = new Stack<Memento>();

		/// <summary>Redo stack</summary>
		private Stack<Memento> redoStack = new Stack<Memento>();
	}
}
